/**
 * AGNCamera
 */

package agngui.agnmodel;
import java.text.DecimalFormat;

import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.swing.JTextField;
import javax.vecmath.AxisAngle4d;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

import com.sun.j3d.utils.universe.SimpleUniverse;

/**
 * Facilitates the camera controls.
 */
public class AGNCamera {
	/**
	 * The format string for displaying camera location values
	 */
	private static DecimalFormat df = new DecimalFormat("0.####");
	
	/**
	 * The initial distance along the Z axis for the camera
	 */
	final double INITIAL_CAMERA_DISTANCE = 1000;
	
    /**
     * the transformation group that tells the camera how to move around in the
     * Java3D Environment
     */
    private TransformGroup transformGroup;
    
    /**
     * the location of the camera
     */
	private Point3d location;
	
	/**
	 * the point that the camera is focused upon
	 */
    private Point3d lookAt;
    
    /**
     * the up vector of the camera
     */
    private Vector3d up;
    
    /**
     * the text field that should update with the camera's x position
     */
	private JTextField camera_x;
	
	/**
     * the text field that should update with the camera's y position
     */
	private JTextField camera_y;
	
	/**
     * the text field that should update with the camera's z position
     */
	private JTextField camera_z;
	
    /**
     * AGNCamera
     *
     * Initializes the camera to the default position (0.0, 0.0, INITIAL_CAMERA_DISTANCE) looking
     * at the origin.
     * 
     * @param universe Reference to the universe to transform.
     */
    public AGNCamera( SimpleUniverse universe ) {
        Transform3D initialTransform = new Transform3D();

        location = new Point3d( 0, 0, INITIAL_CAMERA_DISTANCE );
        lookAt   = new Point3d( 0, 0, 0 );
        up       = new Vector3d( 0, 1, 0 );

        initialTransform.lookAt( location, lookAt, up );

        transformGroup = universe.getViewingPlatform().getViewPlatformTransform();
        transformGroup.setTransform( initialTransform );
    }

    /**
     * AGNCamera
     * 
     * Initializes the camera.
     * 
     * @param location Specifies the initial location of the camera.
     * @param lookAt   Specifies the point the camera is looking at.
     * @param up       Where is "up" in camera space?
     * @param universe The universe containing the transform to modify
     */
    public AGNCamera( Point3d location, Point3d lookAt, Vector3d up, SimpleUniverse universe ) {
        Transform3D initialTransform = new Transform3D();

        this.location = location;
        this.lookAt   = lookAt;
        this.up       = up;

        up.normalize();

        initialTransform.lookAt( location, lookAt, up );

        transformGroup = universe.getViewingPlatform().getViewPlatformTransform();
        transformGroup.setTransform( initialTransform );
    }

    /**
     * roll
     *
     * Rolls by a specific amount (rotate around the lookAt vector)
     *
     * @param theta The angle (in degrees) to rotate around the lookAt vector
     */
    public void roll( double theta ) {
        Vector3d    rollAxis;
        AxisAngle4d axisAngle;
        Transform3D rollTransform = new Transform3D();
        Transform3D currentTransform = new Transform3D();

        rollAxis = new Vector3d( lookAt );
        rollAxis.sub( location );
        rollAxis.normalize();

        // Convert to radians-- negate because AxisAngle goes counter-clockwise
        theta    *= Math.PI / 180.0;
        axisAngle = new AxisAngle4d( rollAxis, theta );

		rollTransform.setRotation( axisAngle );
        rollTransform.transform( up, up );
		
        transformGroup.getTransform( currentTransform );
        currentTransform.lookAt( location, lookAt, up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );
    }

    /**
     * pitch
     * 
     * Pitches by a specific amount (rotate around the right vector)
     *
     * @param theta The angle (in degrees) by which to rotate around the right vector
     */
    public void pitch( double theta ) {
        Vector3d right      = new Vector3d();
        Vector3d lookAtVector;
        AxisAngle4d axisAngle;
        Transform3D pitchTransform   = new Transform3D();
        Transform3D currentTransform = new Transform3D();

        lookAtVector = new Vector3d( lookAt );
        lookAtVector.sub( location );
        //lookAtVector.normalize();

        // Convert to radians -- negate because AxisAngle goes counter-clockwise
        theta    *= -Math.PI / 180.0;
        right.cross( up, lookAtVector );      
        axisAngle = new AxisAngle4d( right, theta );
    
        pitchTransform.setRotation( axisAngle );
        pitchTransform.transform( up, up );
        pitchTransform.transform( lookAtVector, lookAtVector );

        lookAt.add( location, lookAtVector );
        
        transformGroup.getTransform( currentTransform );
        currentTransform.lookAt( location, lookAt, up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );
          
    }

    /**
     * yaw
     * 
     * Rotate around the up vector by some angle.
     *
     * @param theta The angle (in degrees) by which to rotate around the up vector
     */
    public void yaw( double theta ) {
        Vector3d    lookAtVector;
        AxisAngle4d axisAngle;
        Transform3D yawTransform     = new Transform3D();
        Transform3D currentTransform = new Transform3D();

        lookAtVector = new Vector3d( lookAt );
        lookAtVector.sub( location );

        // Convert to radians-- negate because AxisAngle goes counter-clockwise
        theta    *= -Math.PI / 180.0;
        axisAngle = new AxisAngle4d( up, theta );

        yawTransform.setRotation( axisAngle );
        yawTransform.transform( lookAtVector, lookAtVector );

        lookAt.add( location, lookAtVector );

        transformGroup.getTransform( currentTransform );
        currentTransform.lookAt( location, lookAt, up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );
    }

    /**
     * translate
     *
     * Displace the camera location.
     *
     * @param displacement The vector by which to displace the camera origin location.
     */
    public void translate( Vector3d displacement ) {
        Transform3D currentTransform = new Transform3D();

        location.add( displacement );
        lookAt.add( displacement );

		updateFields();
		
        transformGroup.getTransform( currentTransform );
        currentTransform.lookAt( location, lookAt, up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );
    }

    /**
     * zoom
     *
     * Zoom in on the lookAt vector by a specified distance.
     *
     * @param times How many "times" we want to zoom (2x, 3x, etc.)
     */
    public void zoom( double times ) {
        Transform3D currentTransform = new Transform3D();
        Vector3d displacement;// = new Vector3d( lookAt );
        Vector3d lookAtVector;
        
        lookAtVector = new Vector3d( lookAt );
        lookAtVector.sub( location );

        if (times == 0 || Math.abs( times ) < 1.0 ) {
            times = 1.0;
        } else if (times < 0.0 ) {
            // Permit for -x times
            times = -1/times;
        }
        
        displacement = new Vector3d( lookAtVector );
        displacement.scale( 1 - (1/times) );
        location.add( displacement );
		
		updateFields();

        transformGroup.getTransform( currentTransform );
        currentTransform.lookAt( location, lookAt, up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );
    } 
	
	/**
	 * Locks the observer's location, and moves the lookAt around them in the sphere defined by
	 * the center as the observer, and the radius as the lookAt.
	 * 
	 * @param degrees the amount of degrees to move
	 * @param horizontal true if the movement should be horizontal, false if vertical
	 */
	public void rotate( double degrees, boolean horizontal ) {
		Transform3D currentTransform = new Transform3D();
		Transform3D rotateTransform = new Transform3D();
        AxisAngle4d axisAngle = null;
		Vector3d axis_of_rotation = null;
		Vector3d lookAtVector = new Vector3d();
		lookAtVector.sub(lookAt, location);
		
		double theta = degrees * Math.PI/180;
		
		if(horizontal) {
			axis_of_rotation = new Vector3d( up );
		} else {
			axis_of_rotation = new Vector3d();
			axis_of_rotation.cross(up, lookAtVector);
		}
		
		axisAngle = new AxisAngle4d(axis_of_rotation, theta);
		
		rotateTransform.setRotation(axisAngle);
		rotateTransform.transform(lookAtVector, lookAtVector);//rotate the lookAt
		rotateTransform.transform(up, up);//rotate the up vector with the lookAt
		
		lookAt = new Point3d(lookAtVector);
		lookAt.add(location);
		
		transformGroup.getTransform(currentTransform);
		currentTransform.lookAt( location, lookAt, up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );
	}
	
	/**
	 * Locks the look-at, and moves the observer on the sphere defined by the distance from the
	 * observer to the lookat point, in the specified plane (horizontal or vertical)
	 * 
	 * @param degrees The amount of degrees to move
	 * @param horizontal true if movement should be horizontal, false if vertical
	 */
	public void pan( double degrees, boolean horizontal ) {
		Transform3D currentTransform = new Transform3D();
		Transform3D panTransform = new Transform3D();
        AxisAngle4d axisAngle = null;
		Vector3d axis_of_rotation = null;
		Vector3d lookAtVector = new Vector3d();
		lookAtVector.sub(lookAt, location);
		
		double theta = degrees * Math.PI/180;
		
		if(horizontal) {
			axis_of_rotation = new Vector3d( up );
		} else {
			axis_of_rotation = new Vector3d();
			axis_of_rotation.cross(up, lookAtVector);
		}
		
		axisAngle = new AxisAngle4d(axis_of_rotation, theta);
		
		panTransform.setRotation(axisAngle);
		panTransform.transform(location, location);//rotate the location about the lookAt
		panTransform.transform(up, up);//rotate the up vector with the location
		
		transformGroup.getTransform(currentTransform);
		currentTransform.lookAt( location, lookAt, up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );
		
		updateFields();
	}
	
	/**
	 * sets the JTextFields associated with the camera's location to
	 * the values of the camera's x, y, and z coordinates.
	 */
	private void updateFields() {
		camera_x.setText(df.format(location.getX()));
		camera_y.setText(df.format(location.getY()));
		camera_z.setText(df.format(location.getZ()));
	}

    /**
     * sets the location of the camera
     * @param location The new location of the camera
     */
    public void setLocation( Point3d location ) {
        Transform3D currentTransform = new Transform3D();
        this.location = location;
        
		updateFields();
		
        transformGroup.getTransform( currentTransform );
        currentTransform.lookAt( location, lookAt, up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );        
    }

    /**
     * sets the lookAt of the camera
     * @param lookAt The new lookAt
     */
    public void setLookAt( Point3d lookAt ) {
        Transform3D currentTransform = new Transform3D();
        this.lookAt = lookAt;

        transformGroup.getTransform( currentTransform );
        currentTransform.lookAt( location, lookAt, up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );
    }

    /**
     * sets the Up vector of the camera
     * @param up The new up vector
     */
    public void setUp( Vector3d up ) {
        Transform3D currentTransform = new Transform3D();
        this.up = up;

        transformGroup.getTransform( currentTransform );
        currentTransform.lookAt( location, lookAt , up );
        currentTransform.invert();
        transformGroup.setTransform( currentTransform );
    }
	
	/**
	 * snaps the camera's parameters to the default parameters in the system.
	 * 
	 */
	public void snap() {
		setLocation(new Point3d(0, 0, INITIAL_CAMERA_DISTANCE));
		setLookAt(new Point3d(0, 0, 0));
		setUp(new Vector3d(0, 1, 0));
	}
	
	/**
	 * snaps the camera to be at the given inclination from the system axis (positive z axis)
	 * the angle rotates in the positive y,z plane, rotating about the x axis.
	 * 
	 * @param angle - in degrees
	 */
	public void snapInclination(double angle) {
		setLocation(new Point3d(0, 0, location.distance(new Point3d(0,0,0))));
		setLookAt(new Point3d(0, 0, 0));
		setUp(new Vector3d(0, 1, 0));
		double radians = (Math.PI/180 * angle);
		AxisAngle4d axis_of_rotation = new AxisAngle4d(1.0, 0, 0, radians);
		Transform3D xform = new Transform3D();
		xform.setRotation(axis_of_rotation);
		xform.transform(location);
		xform.transform(up);
		setLocation(location);
		setUp(up);
	}
	
	/**
	 * sets the JTextFields that this camera should update.
	 * 
	 * @param x The camera's x field display
	 * @param y The camera's y field display
	 * @param z The camera's z field display
	 */
	public void setFields(JTextField x, JTextField y, JTextField z) {
		camera_x = x;
		camera_y = y;
		camera_z = z;
	}

    /**
     * returns the camera's location
     * 
     * @return the camera's location
     */
    public Point3d getLocation() {
        return location;
    }

    /**
     * returns the lookAt point of the camera
     * 
     * @return the lookAt point of the camera
     */
    public Point3d getLookAt() {
        return lookAt;
    }
	
	/**
	 * returns the vector from "location" to "lookAt"
	 * 
	 * @return the vector From "location" to "lookAt"
	 */
	public Vector3d getLookAtVector() {
		Vector3d lav = new Vector3d(lookAt);
		lav.sub(location);
        return lav;
    }

	/**
	 * returns the up vector of the camera
	 * 
	 * @return the Up vector of the camera
	 */
    public Vector3d getUp() {
        return up;
    }
}
